/**
 * 
 */
package fr.toussna.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;

import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import fr.toussna.entity.DomainEntity;
import fr.toussna.exception.ObjectNotFoundException;

/**
 * <b>Cette classe est la classe abstraite qui rassemble les méthodes de
 * traitements avec la BD</b>
 * <p>
 * On utilise le design pattern Template pour laisser une partie de l'algorithme
 * aux classe dérivantes. Cette classe aussi dispose d'un cache pour éviter des
 * requêtes directes vers la base de données pour des demandes récurrentes.
 * Cette classe utilise le Spring Jdbc Template qui offre une multitude de
 * fonctions pour des commandes sql préparées.
 * 
 * Toutes classe concrètes d'accès aux données étend cette classe abstraite et
 * redéfinie les méthodes.
 * </p>
 * <p>
 * On utilise un cache du Framework {@link Ehcache} pour stocker les entités et
 * objets métiers très demandés et ainsi augmenter la performance et la
 * disponibilité de l'application.
 * </p>
 * 
 * @author My
 * @version 2.0
 * 
 */
public abstract class AbstractDAO extends JdbcTemplate {

	// **********************************
	// * Attributs *
	// **********************************

	private static final Log LOGGER = LogFactory.getLog(AbstractDAO.class.getName());

	/**
	 * Nom de la region du ehcache
	 */
	private static final String CACHE_NAME = AbstractDAO.class.getName();

	/**
	 * Le cache manager injecté par Spring IOC
	 * 
	 * @see AbstractDAO#setCacheManager(CacheManager)
	 */
	private CacheManager cacheManager;

	// **********************************
	// * Methodes Concretes *
	// **********************************
	/**
	 * Cette méthode accède a la base et récupère un objet métier a partir de
	 * son id ou d'un mot clé. Elle utilise le pattern Temlplate pour exécuter
	 * la requete Sql appropriée. Pour la transformation ResultSet vers Objet on
	 * utilise un RowMapper de Spring
	 * 
	 * @param id
	 *            Généralement c'est l'identifiant de l'objet métier
	 * @return L'objet métier
	 * @throws ObjectNotFoundException
	 */
	public DomainEntity select(Number id) throws ObjectNotFoundException {
		DomainEntity entity = null;
		try {
			entity = queryForObject(getSelectSqlStatement(), new Object[] { id }, getDomainEntityRowMapper());
		} catch (Exception e) {
			LOGGER.error("ObjectNotFoundException with id=" + id, e);
			throw new ObjectNotFoundException("Id does not exist ID=" + id);
		}

		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Used Sql Statement: " + getSelectSqlStatement() + "\n Data extracted: " + entity.toString());
		}
		return entity;
	}

	/**
	 * Insère les données d'un objet dans la base. On utilise
	 * {@link AbstractDAO#getDomainEntityData(DomainEntity)} pour appeler les
	 * getters de l'Entity et extraire les données. Utilise
	 * {@link PreparedStatementCreator)} pour remplir les paramètres a insérer
	 * et {@link KeyHolder} pour stocker les clés primaires de la base de
	 * données.
	 * 
	 * @param entité
	 *            objet a insérer
	 * @return Identifiant généré par la base
	 */
	public Number insert(final DomainEntity entity) {
		/*
		 * int affected =
		 * update(getInsertSqlStatement(),getDomainEntityData(entity));
		 * if(LOGGER.isDebugEnabled()){
		 * LOGGER.debug("Inserting Data for entity:"
		 * +entity.getClass().getSimpleName()
		 * +" \n Colonne: "+Arrays.toString(getDomainEntityData(entity))
		 * +" \n Number of affected element: "+affected); }
		 */

		KeyHolder keyHolder = new GeneratedKeyHolder();
		update(new PreparedStatementCreator() {
			public PreparedStatement createPreparedStatement(Connection connection) throws SQLException {
				PreparedStatement ps = connection.prepareStatement(getInsertSqlStatement(), new String[] { "id" });
				int x = 0;
				for (Object ob : getDomainEntityData(entity)) {
					ps.setObject(++x, ob);
				}
				return ps;
			}
		}, keyHolder);
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Inserting Data for entity:" + entity.getClass().getSimpleName() + " \n Colonne: " + getInsertSqlStatement() + " \n Returned ID: " + keyHolder.getKey());
		}
		return keyHolder.getKey();
	}

	/**
	 * Mis a jour un objet dans la base On utilise
	 * {@link AbstractDAO#getDomainEntityData(DomainEntity)} pour appeler les
	 * Getters de l'entité et extraire les données. Utilise
	 * {@link #update(String)} de Spring template
	 * 
	 * @param entité
	 *            objet a updater
	 */
	public void update(DomainEntity entity) {
		int affected = update(getUpdateSqlStatement(), getDomainEntityData(entity));
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Updating Data for object:" + entity.getClass().getSimpleName() + "\n Colonne: " + Arrays.toString(getDomainEntityData(entity))
					+ "\n Number of affected element: " + affected);
		}
	}

	/**
	 * Supprime un objet de la base Utilise {@link #update(String)} de Spring
	 * Template
	 * 
	 * @param entité
	 *            objet a supprimé
	 */
	public void delete(DomainEntity entity) {
		int affected = update(getDeleteSqlStatement(), new Object[] { entity.getId() });
		// Remove also from cache
		Cache cache = cacheManager.getCache(CACHE_NAME);
		if (cache == null) {
			LOGGER.warn("Cache '" + CACHE_NAME + "' not set. Using default settings.");
			cacheManager.addCache(CACHE_NAME);
			cache = cacheManager.getCache(CACHE_NAME);
		}
		cache.remove(entity.getId()+ entity.getClass().getCanonicalName());
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Deleting object:" + entity.getClass().getSimpleName() + "\n Colonne: " + Arrays.toString(getDomainEntityData(entity)) + "\n Number of affected element: "
					+ affected);
		}
	}

	/**
	 * Cette méthode récupère toutes les entités de la base et renvoi une liste
	 * {@link DomainEntity} On utilise le {@link RowMapper} de Spring pour faire
	 * le Mapping objets et données.
	 * 
	 * @return La lise des {@link DomainEntity}
	 */
	public List<DomainEntity> selectAll() {
		List<DomainEntity> list = query(getSelectAllSqlStatement(), getDomainEntityRowMapper());
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Selectin All Data using statement: " + getSelectAllSqlStatement() + "\n received data: " + list.toString());
		}
		return list;
	}

	/**
	 * Cette méthode récupère toutes les entités de la base qui contiennent un
	 * mot clé et renvoi une liste {@link DomainEntity} On utilise le
	 * {@link RowMapper} de Spring pour faire le Mapping objets et données.
	 * 
	 * @param clé
	 *            Le mot clé a rechercher
	 * @return La lise des {@link DomainEntity}
	 */
	public List<DomainEntity> selectByKey(String key) {
		List<DomainEntity> list = query(getSelectByKeySqlStatement(key), getDomainEntityRowMapper());
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("Selecting by key: " + key + "  Using statement" + getSelectByKeySqlStatement(key) + "\n  Received data: " + list.toString());
		}
		return list;
	}

	/**
	 * Cette méthode vérifie l'existence d'une entité dans le cache et le
	 * récupère. .
	 * 
	 * @param id
	 *            Identifiant de l'entité
	 * @return L'entité.
	 */
	public DomainEntity selectCachedElement(long id, String str) {
		Cache cache = cacheManager.getCache(CACHE_NAME);
		DomainEntity cachedElemet = null;
		String idd= id+str;

		if (cache == null) {
			LOGGER.warn("Cache '" + CACHE_NAME + "' not set. Using default settings.");
			cacheManager.addCache(CACHE_NAME);
			cache = cacheManager.getCache(CACHE_NAME);
		}

		if (cache.isElementInMemory(idd)) {
			Element elem = cache.get(idd);
			cachedElemet = (DomainEntity) elem.getValue();
			if (LOGGER.isDebugEnabled()) {
				LOGGER.debug("Exstracting elements from cache:" + cachedElemet.toString());
			}
		}

		return cachedElemet;
	}

	/**
	 * Cette méthode insère un élément dans le cache
	 * 
	 * @param entité
	 *            L'objet entier a insérer dans le cache.
	 */
	public void insertToCache(DomainEntity entity) {
		Cache cache = cacheManager.getCache(CACHE_NAME);
		if (cache == null) {
			LOGGER.warn("Cache '" + CACHE_NAME + "' not set. Using default settings.");
			cacheManager.addCache(CACHE_NAME);
			cache = cacheManager.getCache(CACHE_NAME);
		}
		cache.put(new Element(entity.getId()+entity.getClass().getCanonicalName(), entity));
		if (LOGGER.isDebugEnabled()) {
			LOGGER.debug("New cached element \n " + "A fresh copy was retrieved " + entity);
		}

	}

	// **********************************
	// * Methodes Abstraites *
	// **********************************
	/**
	 * Méthodes renvoyant la requête Sql pour sélectionner un objet métier
	 * depuis la base de données. Cette méthode est implémentée dans les classes
	 * dérivantes.
	 * 
	 * @return La requête Sql de sélection
	 */
	protected abstract String getSelectSqlStatement();

	/**
	 * Méthodes renvoyant la requête Sql pour insérer un objet métier dans la
	 * base de données. Cette méthode est implémentée dans les classes
	 * dérivantes.
	 * 
	 * @return La requête Sql d'insertion
	 */
	protected abstract String getInsertSqlStatement();

	/**
	 * Méthodes renvoyant la requête Sql pour mettre a jour un objet métier dans
	 * la base de données. Cette méthode est implémentée dans les classes
	 * dérivantes.
	 * 
	 * @return La requête Sql de mise a jour
	 */
	protected abstract String getUpdateSqlStatement();

	/**
	 * Méthodes renvoyant la requête Sql pour supprimer un objet métier dans la
	 * base de données. Cette méthode est implémentée dans les classes
	 * dérivantes.
	 * 
	 * @return La requête Sql de suppression
	 */
	protected abstract String getDeleteSqlStatement();

	/**
	 * Méthodes renvoyant la requête Sql pour sélectionner tous les objets
	 * métiers dans la base de données. Cette méthode est implémentée dans les
	 * classes dérivantes.
	 * 
	 * @return La requête Sql de sélection complète
	 */
	protected abstract String getSelectAllSqlStatement();

	/**
	 * Méthodes renvoyant la requête Sql pour sélectionner un objet métier par
	 * mot clé. Cette méthode est implémentée dans les classes dérivantes.
	 * 
	 * @param key
	 *            Le mot a recherché.
	 * @return La requête Sql de sélection par mot clé
	 */
	protected abstract String getSelectByKeySqlStatement(String key);

	/**
	 * Cette méthode retourne le transformateur adéquat des données Sql vers le
	 * modèles objet. Elle utilise le RowMapper propose par Spring JDBC
	 * {@link #RowMapper}
	 * 
	 * @return DomainEntity adéquat pour chaque objet métier.
	 */
	protected abstract RowMapper<DomainEntity> getDomainEntityRowMapper();

	/**
	 * Retourne un tableau des colonnes de la table correspondant a l'objet.
	 * 
	 * @param entité
	 *            entité à extraire les données
	 * @return table des colonnes
	 */
	protected abstract Object[] getDomainEntityData(DomainEntity entity);

	// **********************************
	// * Getters & Setters *
	// **********************************
	/**
	 * Utiliser par Spring pour instancier le CacheManager
	 * 
	 * @param cacheManager
	 *            Le cacheManager a setter
	 */
	public void setCacheManager(CacheManager cacheManager) {
		this.cacheManager = cacheManager;
	}

}
